DOM事件(DOM Event)是什麼?
當網頁瀏覽者透過點擊按鈕、拖移圖片、滑動頁面等等時都會觸發事件的發生。DOM Event定義了這些事件的型態(像是:click、scroll、submit等),讓我們可以透過JavaScript來監聽與處理這些事件。
綁定事件的方法
Inline Events(過時)
將Events綁定在HTML裡,缺點是HTML與JS混用,以及程式碼不簡潔:
<body>
<input type="button" value="Click me!" onclick="console.log('You Click the button!')">
</body>
The Onclick Property(不太推薦)
透過element.onclick = functionRef
觸發事件發生:
//HTML:
<body>
<input type="button" value="Click me!">
</body>
//JS:
document.querySelector('input').onclick = function() {
console.log('You Click the button!')
}
addEventListener()(推薦常用)
為什麼推薦使用addEventListener()?
首先參數裡的listener可以是匿名函數,再來與上面兩種綁定事件方法的最大不同在於,addEventListener()可以對同一元素一次綁定好幾個listener。
語法:
target.addEventListener(type, listener [, options]);
target.addEventListener(type, listener [, useCapture]);
參數:
- type - 各種事件類型
- listener - 觸發事件後要執行的函式
- options
- capture -
Boolean值,表示listener會在事件捕獲(Capturing)到該EventTarget時觸發 - once -
Boolean值,使用once表示listener最多只調用一次。如果為true,則listener在調用後會自動移除此Event。 - passive
Boolean值,如果設置為true,則這個事件的handler function永遠不會呼叫event.preventDefault(),此可以改善滾屏的效能。
- capture -
- useCapture -
Boolean值,為true的話則是捕獲事件(Event Capturing),設置為false則為事件冒泡(Event Bubbling)
範例:
//HTML:
<body>
<input type="button" value="Click me!">
</body>
//JS:
let callback = function() {
console.log('You Click the button!')
}
document.querySelector('input').addEventListener('click', callback )
Event Capturing and Bubbling(捕獲事件與冒泡事件)
捕獲事件(Event Capturing)
假設我們的EventTarget是li
,在捕獲事件的機制下,事件處理器(event handler)會從根元素開始逐一往下觸發。
捕獲事件中事件處理器的觸發順序為:document
=> body
=> div
=> ul
=> li
冒泡事件(Event Bubbling)
假設我們的EventTarget是li
,在冒泡事件的機制下,事件處理器(event handler)會從此事件目標(EventTarget)開始逐一往上觸發。
冒泡事件中事件處理器的觸發順序為:li
=> ul
=> div
=> body
=> document
Dom Event Flow
圖片來源:W3C
從這張圖來看更能了解事件觸發的順序,假設我們觸發了<td>
,首先會從document一路往下直到找到EventTarget(也就是<td>
),然後事件處理器再從EventTarget逐一往上觸發事件。
一個簡單的原則:先捕獲再冒泡,大多時候事件處理器都在Bubbling Phase時被觸發
範例:
//HTML:
<body>
<div>div
<ul>ul
<li>li</li>
</ul>
</div>
//JS:
let div = document.querySelector('div');
let ul = document.querySelector('ul');
let li = document.querySelector('li');
let divFunc = function() {
alert('div');
}
let ulFunc = function() {
alert('ul');
}
let liFunc = function() {
alert('li');
}
li.addEventListener('click', liFunc)
div.addEventListener('click', divFunc)
ul.addEventListener('click', ulFunc)
結果:clickli
,在Bubbling Phase會先觸發li
,再按照ul
=> div
的順序逐一往上。
當然我們也可以控制事件處理器在何時被觸發,這要用到addEventListener()
的第三個參數useCapture
。平時默認為false
(也就是Event Bubbling),要讓事件處理器在Capture Phase被觸發,將useCapture
加上true
即可。
還是用上面的例子,只是將useCapture改為true:
li.addEventListener('click', liFunc, true)
div.addEventListener('click', divFunc, true)
ul.addEventListener('click', ulFunc, true)
結果:clickli
,在Capture Phase會先觸發最上層的div
,再按照ul
=> li
的順序。
停止事件傳遞
當我們想中斷Capture Phase或Bubbling Phase的傳遞時,可以使用:
Event.stopPropagation()
Event.stopImmediatePropagation()
兩者的差別在於,如果今天EventTarget綁定了好幾個listener,想要停止全部與這個EventTarget有關的listener,就必須用Event.stopImmediatePropagation()
。
範例中增加了li綁定liFunc2()
:
let liFunc = function(e) {
alert('li');
e.stopImmediatePropagation();
}
let divFunc = function() {
alert('div');
}
let ulFunc = function() {
alert('ul');
}
let liFunc2 = function() {
alert('li2');
}
li.addEventListener('click', liFunc)
li.addEventListener('click', liFunc2)
div.addEventListener('click', divFunc)
ul.addEventListener('click', ulFunc)
結果:liFunc()
執行完後,跟它同一EventTarget的liFunc2()
不會被傳遞。
阻止事件預設行為
這邊要講的語法是Event.preventDefault()。也許有些人容易將Event.stopPropagation()與Event.preventD阻止事件預設行為efault()搞混,Event.preventDefault()的功能在於阻止事件預設行為,所以它並不回停止事件的傳遞。
//HTML:
<body>
<form>
<label for="checkBox">Check Box:
<input id="checkBox"type="checkbox">
</label>
</form>
<p></p>
<script src="阻止事件預設行為.js"></script>
</body>
JS:
let checkBox = document.querySelector('#checkBox');
checkBox.addEventListener('click', (e) => {
document.querySelector('p').insertAdjacentHTML('beforeend', 'preventDefault()阻止你check這個box<br>')
e.preventDefault();
})
結果:preventDefault()阻止我們checkbox,但事件的傳遞沒有被中斷。
event.target與event.currentTarget
event.Target
- 指實際觸發事件的元素event.currentTarget
- 指向事件綁定的元素
範例:將事件綁定在ul
元素上。
//HTML:
<head>
<style>
ul {padding: 30px;
margin: 20px;
border: black 2px solid;}
</style>
</head>
<body><ul>這裡是ul的位置
<li>About</li>
<li>Project</li>
<li>Contact</li></ul></body>
//JS:
let ul = document.querySelector('ul');
ul.addEventListener('click', (e) => {
e.target.style.color = 'blue';
e.currentTarget.style.color = 'red';
})
結果:可以發現點選ul
的位置時,li
裡的文字都變為紅色,因為這時e.currentTarget
與e.target
都指向ul
,而當我們再點選li
時,被點選的li
會變成藍色,這是因為e.target
指的是實際觸發事件的元素(也就是包在ul
裡的li
),而e.currentTarget
指向的是事件綁定的元素(也就是ul
)。
事件委派(Event Delegation)
剛剛上面有講到event.target
與event.currentTarget
的差別,以及冒泡事件的原理,這邊來說說什麼是事件委派(Event Delegation)?
如果想讓按鈕click後,在主控台印出"Click!",我們可能會這樣寫:
<button>Click me!</button>
document.querySelector('button').addEventListener('click', ()=>{
console.log('click')})
但當今天我們有好幾個button
要綁定事件處理器,我們可能會這樣寫:
<div id="buttons">
<button>Click me!</button>
<button>Click me!</button>
<!-- buttons... -->
<button>Click me!</button>
</div>
//用for loop遍歷元素、把callback獨立出來
let callback = function() {
console.log('click')
}
let buttons = document.querySelectorAll('button');
for (let button of buttons) {
button.addEventListener('click', callback);
}
這樣是行得通的,但我們每loop一次,就有一個button被綁定事件監聽器。這邊有更好、更有效率的寫法,那是用事件委派(Event Delegation)。事件委派(Event Delegation的用法很簡單,就是將事件監聽器附加到按鈕的"父級",並在單擊按鈕時捕獲冒泡事件。
以我們的例子來說,按鈕的父元素是div
,所以我們這樣寫:
let buttons = document.querySelector('#buttons') //步驟1
buttons.addEventListener('click', (e) => { //步驟2
if (e.target.nodeName === 'BUTTON') { //步驟3
console.log('Click!');
}
});
- 步驟1:
先選取button的父元素(也就是<div id = "#buttons"></div>
) - 步驟2:
記得將事件監聽附加到父元素 - 步驟3:
使用event.target選擇目標元素(e.target會指向<button>
),因為這邊未將button設定className,所以使用e.target.nodeName === 'BUTTON'
去設立條件。
靠事件委派(Event Delegation)讓我們只需綁定一個事件監聽器即可!
this
運用this可以簡化我們的程式碼:
//HTML:
<style>
input {
padding: 50px;
margin: 20px;
font-size: 30px;
}
</style>
</head>
<body>
<input type="button" value="Click">
<input type="button" value="Click">
<input type="button" value="Click">
<input type="button" value="Click">
<input type="button" value="Click">
<script src="DOM-this.js"></script>
</body>
讓changeBackgroundColor()裡的this指向其他的事件觸發元素。
//JS:
let inputs = document.querySelectorAll('input');
let rgbColor = function() {
let r = Math.floor(Math.random() * 256);
let g = Math.floor(Math.random() * 256);
let b = Math.floor(Math.random() * 256);
return `rgb(${r}, ${g}, ${b})`;
}
let changeBackgroundColor = function() {
this.style.backgroundColor = rgbColor();
this.style.color = rgbColor();
}
for (let input of inputs) {
input.addEventListener('click', changeBackgroundColor);
}
結果:
- DOM 的事件傳遞機制:捕獲與冒泡
- 介紹 DOM 及事件流程
- [JS] Event Capturing and Bubbling
- EventTarget.addEventListener() - MDN
- e.currentTarget與e.target的區別
- Event.stopPropagation() - MDN
- Event.preventDefault() - MDN
- Event.stopImmediatePropagation()
- A Simple Explanation of Event Delegation in JavaScript
10.Event Delegation — 事件委派介紹 與 觸發委派的回呼函數